<?php

/** @noinspection PhpUnused */

/**
 * Created by PhpStorm.
 * User: Qch
 * Date: 2016/8/14
 * Time: 16:58
 */

namespace Aoe\Intent\Request\Http;


use Aoe\Intent\Request\Http;
use function byteSubStr;
use function str_random;
use function xorTokens;

class Csrf
{
    /**
     * The name of the HTTP header for sending CSRF token.
     */
    const string CSRF_HEADER = 'X-CSRF-Token';
    /**
     * The length of the CSRF token mask.
     */
    const int CSRF_MASK_LENGTH = 8;
    protected Http $http;
    /**
     * @var string the name of the token used to prevent CSRF. Defaults to '_csrf'.
     * This property is used only when [[enableCsrfValidation]] is true.
     */
    protected string $csrfParam = '_csrf';
    
    protected ?string $_csrfToken;
    
    /**
     * @param Http $http
     */
    public function __construct(Http $http)
    {
        $this->http = $http;
    }
    
    /**
     * ## 生成表单时调用
     *
     * @return array [$key, $value]
     */
    public function getHiddenInput(): array
    {
        return [$this->csrfParam, $this->getCsrfToken()];
    }
    
    /**
     * Returns the token used to perform CSRF validation.
     *
     * This token is a masked version of [[rawCsrfToken]] to prevent [BREACH attacks](http://breachattack.com/).
     * 通过HTML表单隐藏域 或者 HTTP头部传递
     *
     * @param boolean $regenerate
     *
     * @return string
     */
    protected function getCsrfToken(bool $regenerate = false): string
    {
        if ($this->_csrfToken === null || $regenerate) {
            $token = $this->loadCsrfToken();
            if ($regenerate || $token === null) {
                $token = str_random(32);
                $this->http->session->set($this->csrfParam, $token);
            }
            // the mask doesn't need to be very random
            $mask = str_random(self::CSRF_MASK_LENGTH);
            // The + sign may be decoded as blank space later, which will fail the validation
            $this->_csrfToken = str_replace('+', '.', base64_encode($mask . xorTokens($token, $mask)));
        }
        
        return $this->_csrfToken;
    }
    
    protected function loadCsrfToken()
    {
        return $this->http->session->get($this->csrfParam);
    }
    
    /**
     * ## 校验表单时调用
     *
     * @return boolean whether CSRF token is valid. If [[enableCsrfValidation]] is false, this method will return true.
     */
    public function validateCsrfToken(): bool
    {
        $method = $this->http->getMethod();
        
        if (in_array($method, ['GET', 'HEAD', 'OPTIONS'], true)) return true;
        
        
        $trueToken = $this->loadCsrfToken();
        
        return isset($_POST[$this->csrfParam]) &&
               $this->validateCsrfTokenInternal($_POST[$this->csrfParam], $trueToken);
    }
    
    /**
     * Validates CSRF token
     *
     * @param string $token
     * @param string $trueToken
     *
     * @return boolean
     */
    private function validateCsrfTokenInternal(string $token, string $trueToken): bool
    {
        $token = base64_decode(str_replace('.', '+', $token));
        $n     = mb_strlen($token, '8bit');
        
        if ($n <= self::CSRF_MASK_LENGTH) return false;
        
        $mask  = byteSubStr($token, 0, self::CSRF_MASK_LENGTH);
        $token = byteSubStr($token, self::CSRF_MASK_LENGTH, $n - self::CSRF_MASK_LENGTH);
        $token = xorTokens($mask, $token);
        
        return $token === $trueToken;
    }
}